<?php
/*
 * Copyright (c) 2008-2009 Ben Blanks <ben.a.blanks@gmail.com>
 * 
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */
/**
 * Pagination class that does all the calculation and even constructs
 * HTML based on specific options and pagination types that are 
 * required by the users of this class.
 * 
 * This makes it easier to build pagination for projects.
 * 
 * @todo There is an issue with the running pagination if you 
 *       give it an even number for the setsize.
 *       This will need to be fixed.
 * 
 * @package Pagination
 * @version 1.2
 * @author Ben.Blanks
 */
class Pagination {
	/**
	 * The current page number 
	 * @since 1.0
	 * @var integer
	 */
	public $iPageNum;
	/**
	 * The number of items to show per page. 
	 * @since 1.0
	 * @var integer
	 */
	public $iPageSize;
	/**
	 * The number of items in the result set. 
	 * @since 1.0
	 * @var integer
	 */
	public $iNumItems;
	/**
	 * The item to start from for the current page. 
	 * @since 1.0
	 * @var integer
	 */
	public $iStartItem;
	/**
	 * The item to start from for the current page. 
	 * @since 1.0
	 * @var integer
	 */
	public $iSetSize;        // The number of pages to be shown on screen 
	/**
	 * The total number of pages. 
	 * @since 1.0
	 * @var integer
	 */
	public $iTotalPages;
	/**
	 * The total number of block sets that are available. 
	 * @since 1.0
	 * @var integer
	 */
	public $iTotalSets;
	/**
	 * The current block set that we are actually in.
	 * @since 1.0
	 * @var integer
	 */
	public $iCurrentSet;
	/**
	 * The starting page number of the block set type.
	 * @since 1.0
	 * @var integer
	 */
	public $iBlockSetStart;
	/**
	 * The ending page number of the block set type.
	 * @since 1.0
	 * @var integer
	 */
	public $iBlockSetEnd;
	/**
	 * The starting page number of the running set type. 
	 * @since 1.0
	 * @var integer
	 */
	public $iRunningSetStart;
	/**
	 * The ending page number of the running set type.
	 * @since 1.0
	 * @var integer
	 */
	public $iRunningSetEnd;
	
	/**
	 * Iniitalises the class and sets the default settings for the calculations.
	 * 
	 * Useful if you just want the calculation data.
	 * 
	 * @since 1.0
	 * @param integer $iPageNum   The current page that you are on.
	 * @param integer $iPageSize  The number of items being displayed per page.
	 * @param integer $iNumItems  The number of items that are in the result set
	 * @param integer $iSetSize   The number of pages in a set of pages.
	 * @return void
	 */
	function __construct( $iPageNum = NULL , $iPageSize = NULL , $iNumItems = NULL , $iSetSize = NULL ) {
		$this->iPageNum    = (!empty($iPageNum))?$iPageNum:1;
		$this->iPageSize   = (!empty($iPageSize))?$iPageSize:20;
		$this->iNumItems   = (!empty($iNumItems))?$iNumItems:0;
		$this->iSetSize    = (!empty($iSetSize))?$iSetSize:20;
		$this->Calculations(); // Calculates all the Pagination Types.
	}
	
	/**
	 * Runs the calculations for the pagination to be created based on.
	 * 
	 * This is available to the class publicly which means that these values 
	 * are available from a class property call from scripts.
	 * 
	 * This sets the class properties with calculation values.
	 * 
	 * @return void
	 */
	private function Calculations() {
		// iPageNum-1 because we want to calculate page 1 as page 0 so the 
		// starti	ng point is 0 x 30 instead of 1 x 30, otherwise we start at
		// page 30 on the first page. (which is wrong)
		$this->iStartItem   = ( $this->iPageSize * ( $this->iPageNum - 1 ) );
		
		// The total number of pages that are available to the set.
		$this->iTotalPages = ceil( $this->iNumItems / $this->iPageSize );
		
		// The total number of page sets available to select through.
		$this->iTotalSets  = ceil( $this->iTotalPages / $this->iSetSize );
		// The current set number if its ever needed.
		$this->iCurrentSet = abs( ceil( ( $this->iPageNum - $this->iSetSize ) / $this->iSetSize ) ) + 1;
		
		// The Block Set Start Page.
		$this->iBlockSetStart = (($this->iCurrentSet * $this->iSetSize) - $this->iSetSize) + 1;
		$this->iBlockSetEnd   = ($this->iCurrentSet * $this->iSetSize);

		// Running sets calculations
		$this->iRunningSetStart = ceil($this->iPageNum - ceil($this->iSetSize / 2) )+1;
		if( $this->iRunningSetStart < 1 ) {
			$this->iRunningSetStart = 1;
		}
		
		// Substract 1 from the start position as we only want to include # numbers into the 
		// set count otherwise we would get 1 extra that we dont actually want.
		$this->iRunningSetEnd   = ($this->iRunningSetStart-1) + $this->iSetSize;
		if( $this->iRunningSetEnd > $this->iTotalPages ) {
			$this->iRunningSetEnd   = $this->iTotalPages;
			$this->iRunningSetStart = $this->iRunningSetEnd - ($this->iSetSize-1);
		}
		// Fixes the running set start.
		if( $this->iRunningSetStart < 1 ) {
			$this->iRunningSetStart = 1;
		}
	}
	
	/**
	 * Pagination HTML - This returns the html for the pagination to be displayed on the page. 
	 * 
	 * The elements that are displayed in the html can be customised using the sOrder paramter 
	 * of the Options array. You do this by setting the string in a comma seperated list which
	 * dictates which elements are available and in which order.
	 * 
	 * The valid values for this ordering are:
	 *   PREV      - Displays the previous page button
	 *   NEXT      - Displays the next page button
	 *   FIRST     - Displays the first page button
	 *   LAST      - Displays the last page button
	 *   PAGENUMS  - Displays the page numbers in the pagination
	 *   SETSIZES  - Displays the set size modification controls
	 * 
	 * When you are steting the sURL parameter you need to set certain keywords in the positions 
	 * that you wish the current page number and the set size to go. This is essential for you 
	 * to capture this value and send it back into the configuration of this pagination class,
	 * not to mention that without this information you will not be able to pagination through
	 * your resultset on your side.
	 * 
	 * @since 1.0
	 * 
	 * @param string   $sType          The pagination type. expected values (running,set)
	 * @param string   $sURL           The URL Format that will be sent back with keywords replaced.
	 * @param array    $aOptions       A set of configuration options that will be used to build the pagination HTML.
	 * @param string   sPrevious       The string to be displayed in the previous button.
	 * @param string   sFirst          The string to be displayed in the first button.
	 * @param string   sNext           The string to be displayed in the next button.
	 * @param string   sLast           The string to be displayed in the last button.
	 * @param boolean  bShowFirst      Show the first number in the page set.
	 * @param boolean  bShowLast       Show the last number in the page set.
	 * @param boolean  bDisableLinks   Disable the previous,next,first,last button links on first and last pages.
	 * @param integer  iSetSize        The set size as a customisation for the calculations.
	 * @param string   sClassDisabled  The name of the css class that will be set on the li of the disabled elements.
	 * @param string   sClassCurent    The name of the css class that will be set on the li of the current elements.
	 * @param string   sClassSizes     The name of the css class that will be set on the li of the pagesize elements.
	 * @param string   sClassSizeSelected     The name of the css class that will be set on the li of the selected pagesize element.
	 * @param boolean  bSetSizesDropDown      Show the sizes selection as a dropdown
	 * @param string   sOrder          A comma seperated list dicatating the order of the elements.
	 *                                 Expected values are PREV FIRST PAGENUMS LAST NEXT SETSIZES
	 *                                 See above for more information
	 * @param array    aSizes          An array of sizes to be displayed in the SETSIZES placement.
	 * @return string
	 */
	public function HTML( $sType = 'running' , $sURL = '?page=PAGENUM&amp;pagesize=PAGESIZE' , $aOptions = array() ) {
		$aDefaults = array(
			'sPrevious'          => '&lsaquo;', // Supports any string as well as the keyword NUMBER
			'sFirst'             => '&laquo;',  // Supports any string as well as the keyword NUMBER
			'sNext'              => '&rsaquo;', // Supports and string as well as the keyword NUMBER
			'sLast'              => '&raquo;',  // Supports and string as well as the keyword NUMBER
			'bShowFirst'         => true,   // Show the first number in the page set
			'bShowLast'          => true,   // Show the last number in the page set
			'bDisableLinks'      => true, // Disable links at beginning and end
			'iSetSize'           => $this->iSetSize,
			'sOrder'             => 'PREV,FIRST,PAGENUMS,LAST,NEXT,SETSIZES', // Exclude any items you do not wish to display
			'sClassDisabled'     => 'disabled',
			'sClassCurrent'      => 'current',
			'sClassSizes'        => 'page-size',
			'sClassSizeSelected' => 'page-size-selected',
			'aSizes'             => array( 10 , 25 , 50 , 75 , 100 , 150 ),
			'bFormattedCode'     => false,
			'bSetSizesDropDown'  => false,
		);
		foreach( $aOptions as $sKey => $sValue ) {
			$aDefaults[$sKey] = $sValue;
		}
		
		$aButtonOrder = explode( ',' , $aDefaults['sOrder'] );
		
		$this->iSetSize = $aDefaults['iSetSize'];
		
		if( !$aDefaults['bShowFirst'] && in_array( 'PREV' , $aButtonOrder ) ) { $this->iSetSize--; }
		if( !$aDefaults['bShowLast'] && in_array( 'NEXT' , $aButtonOrder ) ) { $this->iSetSize--; }
		
		$this->Calculations(); // Calculates all the Pagination Types.
		
		$iPrevPage = (($this->iPageNum-1) >= 1)?$this->iPageNum-1:1;
		
		if( $sType == 'running' && $this->iPageNum == $this->iTotalPages && $aDefaults['sPrevious'] == 'NUMBER' ) {
			$iPrevPage = (($this->iPageNum-($aDefaults['iSetSize']-1)) >= 1 )?$this->iPageNum-($aDefaults['iSetSize']-1):1;
		}
		
		$iNextPage = (($this->iPageNum+1) <= $this->iTotalPages)?$this->iPageNum+1:$this->iTotalPages;
		
		// IF next is NUMBER and page is 1 and type is running
		if( $sType == 'running' && $this->iPageNum == 1 && $aDefaults['sNext'] == 'NUMBER' ) {
			$iNextPage = (($this->iPageNum+($aDefaults['iSetSize']-1)) <= $this->iTotalPages)?$this->iPageNum+($aDefaults['iSetSize']-1):$this->iTotalPages;
		}
		
		//$bDisplayFirstInPages = ($aDefaults['sPrevious'] != 'NUMBER');
		//$bDisplayLastInPages  = ($aDefaults['sPrevious'] != 'NUMBER');
		
		// NUMBER keyword replace with the PAGE NUMBER
		$aDefaults['sPrevious']  = str_replace( 'NUMBER' , $iPrevPage , $aDefaults['sPrevious'] );
		$aDefaults['sFirst']     = str_replace( 'NUMBER' , 1 , $aDefaults['sFirst'] );
		$aDefaults['sLast']      = str_replace( 'NUMBER' , $this->iTotalPages , $aDefaults['sLast'] );
		$aDefaults['sNext']      = str_replace( 'NUMBER' , $iNextPage , $aDefaults['sNext'] );
		
		$sHTML = '<ul class="pagination">'.$this->EOL($aDefaults['bFormattedCode']);
		
		
		foreach( $aButtonOrder as $sItem ) {
			
			// PREVIOUS link
			if( $sItem == 'PREV' ) {
				
				$sLink = $this->HTML_LinkReplace( $iPrevPage , $sURL );
				$sClass = '';
				$bDisable = false;
				if( ($this->iPageNum-1) < 1 )  {
					if( $aDefaults['bDisableLinks'] ) {
						$bDisable = true;
						$sClass = " class=\"{$aDefaults['sClassDisabled']}\"";
					}
					else {
						$sClass = " class=\"{$aDefaults['sClassCurrent']}\"";	
					}
				}
				
				$sHTML .= ( $bDisable && $aDefaults['bDisableLinks'] )
					? "<li{$sClass}>{$aDefaults['sPrevious']}</li>" 
					: "<li{$sClass}><a href=\"{$sLink}\">{$aDefaults['sPrevious']}</a></li>";
				$sHTML .= $this->EOL($aDefaults['bFormattedCode']);
			}
			
			// FIRST link
			else if( $sItem == 'FIRST' ) {
				
				if( $sType == 'set' && ($this->iBlockSetStart-1) >= 1 ) {
					$sLink = $this->HTML_LinkReplace( ($this->iBlockSetStart-1) , $sURL );
				}
				else {
					$sLink = $this->HTML_LinkReplace( 1 , $sURL );
				}
				$sClass = '';
				$bDisable = false;
				if( ($this->iPageNum-1) < 1 ) {
					if( $aDefaults['bDisableLinks'] ) {
						$bDisable = true;
						$sClass = " class=\"{$aDefaults['sClassDisabled']}\"";
					}
					else {
						$sClass = " class=\"{$aDefaults['sClassCurrent']}\"";	
					}
				}
				
				$sHTML .= ($bDisable && $aDefaults['bDisableLinks'])
					? "<li{$sClass}>{$aDefaults['sFirst']}</li>" 
					: "<li{$sClass}><a href=\"{$sLink}\">{$aDefaults['sFirst']}</a></li>";
				$sHTML .= $this->EOL($aDefaults['bFormattedCode']);
			}
			
			// LAST link
			else if( $sItem == 'LAST' ) {
				
				if( $sType == 'set' && ($this->iBlockSetEnd+1) <= $this->iTotalPages ) {
					$sLink = $this->HTML_LinkReplace( ($this->iBlockSetEnd+1) , $sURL );
				}
				else {
					$sLink = $this->HTML_LinkReplace( $this->iTotalPages , $sURL );
				}
				
				$sClass = '';
				$bDisable = false;
				if( $this->iPageNum == $this->iTotalPages ) {
					if( $aDefaults['bDisableLinks'] ) {
						$bDisable = true;
						$sClass = " class=\"{$aDefaults['sClassDisabled']}\"";
					}
					else {
						$sClass = " class=\"{$aDefaults['sClassCurrent']}\"";	
					}
				}
				
				$sHTML .= ($bDisable && $aDefaults['bDisableLinks'])
					? "<li{$sClass}>{$aDefaults['sLast']}</li>" 
					: "<li{$sClass}><a href=\"{$sLink}\">{$aDefaults['sLast']}</a></li>";
				$sHTML .= $this->EOL($aDefaults['bFormattedCode']);
			}
			
			// NEXT link
			else if( $sItem == 'NEXT' ) {
				
				$sLink = $this->HTML_LinkReplace( $iNextPage , $sURL );
				
				$sClass = '';
				$bDisable = false;
				if( $this->iPageNum == $this->iTotalPages ) {
					if( $aDefaults['bDisableLinks'] ) {
						$bDisable = true;
						$sClass = " class=\"{$aDefaults['sClassDisabled']}\"";
					}
					else {
						$sClass = " class=\"{$aDefaults['sClassCurrent']}\"";	
					}
				}
				
				$sHTML .= ($bDisable && $aDefaults['bDisableLinks'])
					? "<li{$sClass}>{$aDefaults['sNext']}</li>" 
					: "<li{$sClass}><a href=\"{$sLink}\">{$aDefaults['sNext']}</a></li>";
				$sHTML .= $this->EOL($aDefaults['bFormattedCode']);
			}
			
			// PAGENUMS display
			else if( $sItem == 'PAGENUMS' ) {
				
				// Build the running list of page numbers
				if( $sType == 'set' ) {
					//iBlockSetStart
					for( $i = $this->iBlockSetStart; $i <= $this->iBlockSetEnd; $i++ ) {
						$bShow = true;
						// Show First Number
						if( $bShow ) {$bShow = ( ($i == 1 && $aDefaults['bShowFirst']) || $i > 1 );}
						// Show Last Number
						if( $bShow ) {$bShow = ( ($i == $this->iTotalPages && $aDefaults['bShowFirst']) || $i < $this->iTotalPages );}
						
						// Actually Show the Page Number
						if( $bShow ) {
							$sLink = $this->HTML_LinkReplace( $i , $sURL );
							$sClass = '';
							if( $i == $this->iPageNum ) {
								$sClass = " class=\"{$aDefaults['sClassCurrent']}\"";
							}
							$sHTML .= "<li{$sClass}><a href=\"{$sLink}\">{$i}</a></li>";
							$sHTML .= $this->EOL($aDefaults['bFormattedCode']);
						}
					}
					
				}
				else if( $sType == 'running' ) {
					
					if( !$aDefaults['bShowFirst'] && $this->iPageNum <= (ceil( ($this->iSetSize/2) )) ) {
						$this->iRunningSetEnd++;
					}
					if( !$aDefaults['bShowLast'] && $this->iPageNum > ($this->iTotalPages - ceil( ($this->iSetSize/2) )) ) {
						$this->iRunningSetStart--;
					}
					
					for( $i = $this->iRunningSetStart; $i <= $this->iRunningSetEnd; $i++ ) {
						$bShow = true;
						// Show First Number
						if( $bShow ) {$bShow = ( ($i == 1 && $aDefaults['bShowFirst']) || $i > 1 );}
						// Show Last Number
						if( $bShow ) {$bShow = ( ($i == $this->iTotalPages && $aDefaults['bShowFirst']) || $i < $this->iTotalPages );}
						
						// Actually Show the Page Number
						if( $bShow ) {
							$sLink = $this->HTML_LinkReplace( $i , $sURL );
							$sClass = '';
							if( $i == $this->iPageNum ) {
								$sClass = " class=\"{$aDefaults['sClassCurrent']}\"";
							}
							$sHTML .= "<li{$sClass}><a href=\"{$sLink}\">{$i}</a></li>";
							$sHTML .= $this->EOL($aDefaults['bFormattedCode']);
						}
					}
				}
				
			}
			
			// SETSIZES display
			else if( $sItem == 'SETSIZES' ) {

				if( $aDefaults['bSetSizesDropDown'] ) {
					$sHTML .= "<li class=\"{$aDefaults['sClassSizes']}\">";
					$sLink = $this->HTML_LinkReplace( 1 , $sURL , $iSize );
					$sHTML .= "<select name=\"elem_{$aDefaults['sClassSizes']}\" onchange=\"changeSet(this.value);\">";
					foreach( $aDefaults['aSizes'] as $iSize ) {
						$sSelected = ($this->iPageSize == $iSize) ? ' selected="selected"' : '';
						$sHTML .= "<option value=\"{$iSize}\"{$sSelected}>{$iSize}</option>";
					}
					$sHTML .= "</select></li>";
				}
				else { 
					foreach( $aDefaults['aSizes'] as $iSize ) {
						$sLink = $this->HTML_LinkReplace( 1 , $sURL , $iSize );
						$sSelected = ($this->iPageSize == $iSize) ? ' '.$aDefaults['sClassSizeSelected'] : '';
						$sHTML .= "<li class=\"{$aDefaults['sClassSizes']}{$sSelected}\"><a href=\"{$sLink}\">{$iSize}</a></li>";
						$sHTML .= $this->EOL($aDefaults['bFormattedCode']);
					}
				}
			}
		}

		$sHTML .= '</ul>';
		if( $aDefaults['bSetSizesDropDown'] ) {
			// Javascript for the dropdown functionality for setsizes
			$sHTML .= "<script type=\"text/javascript\">";
			$sHTML .= "function changeSet( iPageSize ) {";
			$sHTML .= "var sURLFormat = '{$sURL}';";
			$sHTML .= "var sURL = sURLFormat;";
			$sHTML .= "sURL = sURL.replace( '&amp;'  , '&' );";
			$sHTML .= "sURL = sURL.replace( 'PAGENUM'  , 1 );";
			$sHTML .= "sURL = sURL.replace( 'PAGESIZE' , iPageSize );";
			$sHTML .= "window.location = sURL;";
			$sHTML .= "}";
			$sHTML .= "</script>";
		}
		$sHTML .= $this->EOL($aDefaults['bFormattedCode']);
		return $sHTML;
	}
	
	private function EOL( $bDoIt = true ) {
		if( $bDoIt ) {
			return PHP_EOL;
		}
		return '';
	}
	
	/**
	 * Utility method that is used to replace the keywords in the links.
	 * 
	 * @since 1.0
	 * @param unknown_type $sPageNum
	 * @param unknown_type $sURL
	 * @param unknown_type $iPageSize
	 * @return unknown_type
	 */
	private function HTML_LinkReplace( $sPageNum , $sURL , $iPageSize = false ) {
		if( !$iPageSize ) {
			$iPageSize = $this->iPageSize;
		}
		$sLink = str_replace( 'PAGENUM' , $sPageNum , $sURL );
		$sLink = str_replace( 'PAGEITEMS' , $iPageSize , $sLink );
		$sLink = str_replace( 'PAGESIZE' , $iPageSize , $sLink );
		return $sLink;
	}
}
